iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
JavaScript

JavaScript為什麼筆記本系列 第 25 篇

Day 25|Hoisting 真的是把程式碼搬到最上面嗎?為什麼 function 可以先用,const 卻不行?

  • 分享至 

  • xImage
  •  

Day 24 講 Scope 時,我們留下一個函式:

sayHello()

function sayHello() {
  console.log('Hello')
}

照我平常讀程式的方式,JavaScript 應該是從上往下跑。

那第一行執行 sayHello() 的時候,下面那個 function 明明還沒走到,為什麼可以呼叫?

很多教學會回答:

因為 Hoisting,JavaScript 會把宣告搬到最上面。

但如果真的只是「搬到最上面」,那這段又要怎麼解釋?

sayHello()

const sayHello = () => {
  console.log('Hello')
}

一樣都是函式。

一個可以先呼叫,一個卻不行。

所以今天我真正想問的是:

Hoisting 到底發生了什麼?


比較接近的畫面是:

真正開始一行一行執行以前
↓
JavaScript 先建立這個 Scope 會用到的名稱
↓
不同宣告方式,建立後的初始狀態不同
↓
才開始照原本順序執行程式

所以「Hoisting」比較像我們看到的現象名稱。


先看 function declaration

sayHello()

function sayHello() {
  console.log('Hello')
}

結果可以正常執行。

如果只看執行順序,很像:

第 1 行
sayHello()

第 3 行
才出現 function sayHello

但在真正執行第一行以前,JavaScript 已經先處理這個 Scope 裡的宣告。

對 function declaration 來說,可以先把它想成:

建立 sayHello 這個名稱
↓
而且已經讓它指向那個 function
↓
開始執行程式

所以走到:

sayHello()

時,這個名稱已經找得到,而且已經是可呼叫的 function。


所以不是 function 被搬到上面

原始碼仍然是:

sayHello()

function sayHello() {
  console.log('Hello')
}

不是偷偷變成:

function sayHello() {
  console.log('Hello')
}

sayHello()

第二段只是很常見的教學比喻。

它可以幫忙記結果,但如果把比喻當成真的,就會在 let、const、var 出現時開始亂掉。


那 var 為什麼更怪?

看這段:

console.log(count)

var count = 10

它不是直接說 count 完全不存在。

常見結果反而是:

undefined

明明:

var count = 10

還沒執行,為什麼 count 已經存在?

因為在進入執行階段以前,var 的名稱也已經先被建立。

但它和 function declaration 不一樣。

可以先粗略想成:

var count
↓
名稱先建立
↓
初始值先是 undefined
↓
真正執行到 count = 10
↓
才變成 10

所以:

console.log(count)
var count = 10

和:

console.log(count)
let count = 10

結果會不一樣。


let 和 const 也有 Hoisting 嗎?

這裡最容易被一句「let / const 不會 Hoist」帶歪。

看:

console.log(count)

const count = 10

它會出錯。

於是很容易得到結論:

const 沒有 Hoisting。

但更精確的理解是:

const 的名稱在這個 Scope 建立時也已經被處理,只是在執行到宣告那一行以前,不能被使用。

這段從 Scope 開始,到實際執行宣告之前的區域,常被稱為:

Temporal Dead Zone
TDZ

所以它不是:

count 完全不存在

而比較像:

count 這個 binding 已經建立
↓
但現在還不能存取
↓
執行到 const count = 10
↓
完成初始化
↓
之後才能使用

var 和 const 的差異終於有畫面了

把兩個放在一起:

console.log(a)
var a = 10

可以先想成:

建立 a
↓
a = undefined
↓
開始執行
↓
console.log(a)
→ undefined
↓
a = 10

而:

console.log(b)
const b = 10

比較像:

建立 b
↓
尚未初始化
↓
開始執行
↓
console.log(b)
→ 不能存取
↓
執行 const b = 10
↓
完成初始化

這樣比「var 會 Hoist,const 不會」更能解釋實際行為。


那 function expression 為什麼不能先呼叫?

回到 Day 23:

const sayHello = function () {
  console.log('Hello')
}

或:

const sayHello = () => {
  console.log('Hello')
}

這兩種寫法本質上都有一件事:

先有一個變數 sayHello
↓
再把 function value 放進去

所以如果我寫:

sayHello()

const sayHello = () => {
  console.log('Hello')
}

真正卡住的不是「arrow function 不會 Hoist」。

而是:

sayHello 是 const binding
↓
執行到宣告以前仍在 TDZ
↓
不能先存取

這突然把 Day 23 和 Day 25 接起來了。


function declaration 和 function expression 差在哪裡?

現在可以畫成:

function declaration

function sayHello() {}

建立 Scope 時
↓
sayHello 已經能對到 function
↓
所以前面可以呼叫

對比:

function expression / arrow function

const sayHello = () => {}

建立 Scope 時
↓
先處理 const sayHello
↓
但還沒執行到初始化
↓
不能先使用

所以差異不是:

一個是 function,一個不是 function。

它們最後都可以得到 function value。

真正不同的是:

這個 function value 是用哪一種宣告方式綁到名稱上的?


Hoisting 和 Scope 其實是同一條線上的問題

Day 24 才剛學:

Scope 決定某個名稱在哪裡找得到。

Day 25 其實是在問更前面一步:

進入這個 Scope 時,這些名稱是什麼時候建立的?

所以可以串成:

進入 Scope
↓
建立這個 Scope 的 bindings
↓
不同宣告有不同初始化狀態
↓
開始執行程式
↓
查找名稱時沿 Scope Chain 尋找

Scope 和 Hoisting 原來不是兩個互不相干的章節。

它們都在處理:

名字什麼時候存在?
名字現在能不能用?
名字要去哪裡找?

最小實驗:四種宣告放一起看

1. Function declaration

hello()

function hello() {
  console.log('hello')
}

預期:可以呼叫。

2. var

console.log(a)

var a = 10

預期:先看到 undefined。

3. let

console.log(b)

let b = 10

預期:在初始化前存取會出錯。

4. const + arrow function

hello()

const hello = () => {
  console.log('hello')
}

預期:在初始化前存取 hello 會出錯。

光看這四段,就比背一句「Hoisting 是往上搬」完整很多。


最容易搞錯的地方

誤解 1:Hoisting 就是程式碼真的被移動

不是。

這是一個幫助理解現象的說法,不是原始碼真的重新排列。

誤解 2:let / const 完全沒有 Hoisting

太粗略。

它們的 binding 會在 Scope 建立時被處理,但在宣告執行完成前處於不能存取的狀態。

誤解 3:function expression 不能先呼叫,是因為 function 比較晚建立

不完全是。

關鍵要看它綁在 var、let 還是 const 上。

誤解 4:var 的 undefined 代表我真的有寫 undefined

不是。

那是它在執行真正賦值前的初始狀態。


如果今天只記得一件事

Hoisting 不要想成「把程式碼搬到上面」,而要想成「JavaScript 在執行程式以前,會先建立這個 Scope 裡的名稱;不同宣告的初始化方式不一樣」。

這樣記:

function declaration
→ 名稱與 function 都先準備好

var
→ 名稱先準備好,初始是 undefined

let / const
→ 名稱先建立,但初始化前不能使用

上一篇
Day 24|Scope 到底在限制什麼?為什麼有些變數出了大括號就消失了?
下一篇
Day 26|外面的 function 都跑完了,裡面的 count 為什麼還記得?
系列文
JavaScript為什麼筆記本 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言